package net.mycrub.bitwise.core.impl;

import net.mycrub.bitwise.core.ILayer;
import net.mycrub.bitwise.core.IRetroAction;
import net.mycrub.bitwise.core.IRetroActionCallback;
import net.mycrub.bitwise.core.RetroActionType;
import net.mycrub.bitwise.core.util.Random;

import org.apache.commons.math.linear.Array2DRowRealMatrix;
import org.apache.commons.math.linear.RealMatrix;
import org.simpleframework.xml.Attribute;
import org.simpleframework.xml.ElementArray;
import org.simpleframework.xml.Root;

@Root
public class LinearLayer extends AbstractLayer {

    private static final double MUTATION_CHANCE = .001;
    private static final double COMBINATION_CHANCE = .5;
    private static final double RETROACTION_AMPLITUDE = .3;

    private RealMatrix operator;
    private RealMatrix mutator;

    @Attribute(required = false)
    private double mutationChance = MUTATION_CHANCE;
    @Attribute(required = false)
    private double combinationChance = COMBINATION_CHANCE;
    @Attribute(required = false)
    private double retroactionAmplitude = RETROACTION_AMPLITUDE;

    public LinearLayer(@ElementArray(name = "operator") double[][] operator,
            @ElementArray(name = "mutator") double[][] mutator) {
        this.operator = new Array2DRowRealMatrix(operator);
        this.mutator = new Array2DRowRealMatrix(mutator);

        if (this.operator.getRowDimension() != this.mutator.getRowDimension()) {
            throw new IllegalArgumentException(
                    "Operator and mutator have different row domensions");
        }
        if (this.operator.getColumnDimension() != this.mutator
                .getColumnDimension()) {
            throw new IllegalArgumentException(
                    "Operator and mutator have different column domensions");
        }
    }

    @ElementArray
    public double[][] getOperator() {
        return operator.getData();
    }

    @ElementArray
    public double[][] getMutator() {
        return mutator.getData();
    }

    public double getMutationChance() {
        return mutationChance;
    }

    public void setMutationChance(double mutationChance) {
        this.mutationChance = mutationChance;
    }

    public double getCombinationChance() {
        return combinationChance;
    }

    public void setCombinationChance(double combinationChance) {
        this.combinationChance = combinationChance;
    }

    public double getRetroactionAmplitude() {
        return retroactionAmplitude;
    }

    public void setRetroactionAmplitude(double retroactionAmplitude) {
        this.retroactionAmplitude = retroactionAmplitude;
    }

    /** {@inheritDoc} */
    public double[] process(double[] input, IRetroActionCallback callback) {
        double[] output = operator.operate(input);

        callback.setRetroAction(new LinearRetroAction(input, output));

        return output;
    }

    /** {@inheritDoc} */
    public void mutate() {
        for (int r = 0; r < operator.getRowDimension(); r++) {
            for (int c = 0; c < operator.getColumnDimension(); c++) {
                if (Random.next() < mutationChance) {
                    double m = mutator.getEntry(r, c);
                    double mo = 1 + m * (2 * Random.next() - 1);
                    operator.setEntry(r, c, operator.getEntry(r, c) * mo);

                    double mm = 1 + m * (2 * Random.next() - 1);
                    mutator.setEntry(r, c, mutator.getEntry(r, c) * mm);
                }
            }
        }
    }

    /** {@inheritDoc} */
    public ILayer combine(ILayer partner) {
        if (!LinearLayer.class.isAssignableFrom(partner.getClass())) {
            throw new IllegalArgumentException(
                    "Layers are not compatible for combination "
                            + this.getClass().getName() + "/"
                            + partner.getClass().getName());
        }
        LinearLayer lPartner = (LinearLayer) partner;

        double[][] newOperator = new double[operator.getRowDimension()][operator
                .getColumnDimension()];
        double[][] newMutator = new double[mutator.getRowDimension()][mutator
                .getColumnDimension()];

        for (int r = 0; r < operator.getRowDimension(); r++) {
            for (int c = 0; c < operator.getColumnDimension(); c++) {
                if (Random.next() > combinationChance) {
                    newOperator[r][c] = operator.getEntry(r, c);
                } else {
                    newOperator[r][c] = lPartner.operator.getEntry(r, c);
                }
                if (Random.next() > combinationChance) {
                    newMutator[r][c] = mutator.getEntry(r, c);
                } else {
                    newMutator[r][c] = lPartner.mutator.getEntry(r, c);
                }
            }
        }

        return new LinearLayer(newOperator, newMutator);
    }

    /** {@inheritDoc} */
    public int getInputSize() {
        return operator.getColumnDimension();
    }

    /** {@inheritDoc} */
    public int getOutputSize() {
        return operator.getRowDimension();
    }

    /** {@inheritDoc} */
    public ILayer duplicate() {
        return new LinearLayer(operator.getData(), mutator.getData());
    }

    private class LinearRetroAction implements IRetroAction {
        private int maxI = 0;
        private int maxO = 0;

        public LinearRetroAction(double[] input, double[] output) {
            for (int i = 0; i < input.length; i++) {
                if (i == 0 || input[i] > input[maxI]) {
                    maxI = i;
                }
            }

            for (int o = 0; o < output.length; o++) {
                if (o == 0 || output[o] > output[maxO]) {
                    maxO = o;
                }
            }
        }

        public void process(RetroActionType type) {
            double action = 1;
            switch (type) {
            case POSITIVE:
                action += retroactionAmplitude;
                break;
            case NEGATIVE:
                action -= retroactionAmplitude;
                break;
            default:
            }
            operator.setEntry(maxO, maxI, operator.getEntry(maxO, maxI) * action);
        }
    }

}
